﻿// --------------------------------------------------------------------------------------------------------------------
// <copyright file="BindService.cs" company="">
//   
// </copyright>
// <summary>
//   The bind service.
// </summary>
// --------------------------------------------------------------------------------------------------------------------

namespace Libium.Bindings
{
    using System;
    using System.Collections.Generic;

    using Libium.Adapters;

    /// <summary>
    ///   The bind service.
    /// </summary>
    public class BindService : IBindService
    {
        #region Fields

        /// <summary>
        ///   The _binders.
        /// </summary>
        private readonly Dictionary<Type, Dictionary<Type, IBinder>> _binders =
            new Dictionary<Type, Dictionary<Type, IBinder>>();

        /// <summary>
        ///   The _bindings.
        /// </summary>
        private readonly List<IBinding> _bindings = new List<IBinding>();

        /// <summary>
        ///   The _parent.
        /// </summary>
        private IBindService _parent;

        #endregion

        #region Public Properties

        /// <summary>
        ///   Gets or sets the adapt service.
        /// </summary>
        public IAdaptService AdaptService { get; set; }

        /// <summary>
        ///   Gets or sets a value indicating whether auto bind.
        /// </summary>
        public bool AutoBind { get; set; }

        // TODO Rename
        /// <summary>
        ///   Gets or sets the parent.
        /// </summary>
        public IBindService Parent
        {
            get
            {
                if (this._parent == null)
                {
                    return this;
                }

                return this._parent;
            }

            set
            {
                this._parent = value;
            }
        }

        #endregion

        #region Public Methods and Operators

        /// <summary>
        /// The add binder.
        /// </summary>
        /// <param name="binder">
        /// The binder. 
        /// </param>
        public void AddBinder(IBinder binder)
        {
            Dictionary<Type, IBinder> typeBinders;

            this._binders.TryGetValue(binder.Type1, out typeBinders);
            if (typeBinders == null)
            {
                typeBinders = new Dictionary<Type, IBinder>();
                this._binders.Add(binder.Type1, typeBinders);
            }

            typeBinders.Add(binder.Type2, binder);

            this._binders.TryGetValue(binder.Type2, out typeBinders);
            if (typeBinders == null)
            {
                typeBinders = new Dictionary<Type, IBinder>();
                this._binders.Add(binder.Type2, typeBinders);
            }

            typeBinders.Add(binder.Type1, binder);
        }

        /// <summary>
        /// The add binder.
        /// </summary>
        /// <param name="createBindingFunc">
        /// The create binding func. 
        /// </param>
        /// <typeparam name="T1">
        /// </typeparam>
        /// <typeparam name="T2">
        /// </typeparam>
        public void AddBinder<T1, T2>(Func<IBindService, T1, T2, IBinding<T1, T2>> createBindingFunc) where T1 : class
            where T2 : class
        {
            this.AddBinder(new FuncBinder<T1, T2>(createBindingFunc));
        }

        // TODO Add cache for result

        /// <summary>
        /// The get binder.
        /// </summary>
        /// <param name="type1">
        /// The type 1. 
        /// </param>
        /// <param name="type2">
        /// The type 2. 
        /// </param>
        /// <returns>
        /// The <see cref="IBinder"/> . 
        /// </returns>
        public IBinder GetBinder(Type type1, Type type2)
        {
            var types1 = this.GetPrioritizedTypes(type1);
            var types2 = this.GetPrioritizedTypes(type2);

            IBinder binder = null;

            foreach (var t1 in types1)
            {
                Dictionary<Type, IBinder> typeBinders;
                if (!this._binders.TryGetValue(t1, out typeBinders))
                {
                    continue;
                }

                foreach (var t2 in types2)
                {
                    if (typeBinders.TryGetValue(t2, out binder))
                    {
                        return binder;
                    }
                }
            }

            return binder;
        }

        /// <summary>
        /// The try bind.
        /// </summary>
        /// <param name="obj1">
        /// The obj 1. 
        /// </param>
        /// <param name="obj2">
        /// The obj 2. 
        /// </param>
        /// <param name="binding">
        /// The binding. 
        /// </param>
        /// <returns>
        /// The <see cref="bool"/> . 
        /// </returns>
        public virtual bool TryBind(object obj1, object obj2, out IBinding binding)
        {
            binding = null;

            var type1 = obj1.GetType();
            var type2 = obj2.GetType();

            var binder = this.GetBinder(type1, type2);

            if (binder == null)
            {
                if (this.AutoBind)
                {
                    binding = this.CreateAutoBinding(this.Parent, obj1, obj2);
                }
            }
            else
            {
                binding = binder.CreateBinding(this.Parent, obj1, obj2);
            }

            if (binding == null)
            {
                return false;
            }

            binding.Bind();
            this._bindings.Add(binding);

            return true;
        }

        /// <summary>
        /// The try unbind.
        /// </summary>
        /// <param name="obj1">
        /// The obj 1. 
        /// </param>
        /// <param name="obj2">
        /// The obj 2. 
        /// </param>
        /// <returns>
        /// The <see cref="bool"/> . 
        /// </returns>
        public bool TryUnbind(object obj1, object obj2)
        {
            var binding = this.GetBinding(obj1, obj2);
            if (binding == null)
            {
                return false;
            }

            binding.Unbind();
            return true;
        }

        /// <summary>
        /// The try unbind.
        /// </summary>
        /// <param name="obj">
        /// The obj. 
        /// </param>
        /// <returns>
        /// The <see cref="bool"/> . 
        /// </returns>
        public bool TryUnbind(object obj)
        {
            var bindings = this.GetBindings(obj);
            if (bindings == null || bindings.Count == 0)
            {
                return false;
            }

            foreach (var binding in bindings)
            {
                binding.Unbind();
            }

            return true;
        }

        #endregion

        #region Methods

        /// <summary>
        /// The create auto binding.
        /// </summary>
        /// <param name="service">
        /// The service. 
        /// </param>
        /// <param name="obj1">
        /// The obj 1. 
        /// </param>
        /// <param name="obj2">
        /// The obj 2. 
        /// </param>
        /// <returns>
        /// The <see cref="IBinding"/> . 
        /// </returns>
        protected virtual IBinding CreateAutoBinding(IBindService service, object obj1, object obj2)
        {
            return new AutoBinding<object, object>(service, obj1, obj2);
        }

        /// <summary>
        /// The get binding.
        /// </summary>
        /// <param name="obj1">
        /// The obj 1. 
        /// </param>
        /// <param name="obj2">
        /// The obj 2. 
        /// </param>
        /// <returns>
        /// The <see cref="IBinding"/> . 
        /// </returns>
        protected IBinding GetBinding(object obj1, object obj2)
        {
            foreach (var binding in this._bindings)
            {
                if (binding.Object1 == obj1 && binding.Object2 == obj2
                    || binding.Object1 == obj2 && binding.Object2 == obj1)
                {
                    return binding;
                }
            }

            return null;
        }

        /// <summary>
        /// The get bindings.
        /// </summary>
        /// <param name="obj">
        /// The obj. 
        /// </param>
        /// <returns>
        /// The <see cref="IList"/> . 
        /// </returns>
        protected IList<IBinding> GetBindings(object obj)
        {
            return null;
        }

        /// <summary>
        /// The get prioritized types.
        /// </summary>
        /// <param name="type">
        /// The type. 
        /// </param>
        /// <returns>
        /// The <see cref="IEnumerable"/> . 
        /// </returns>
        private IEnumerable<Type> GetPrioritizedTypes(Type type)
        {
            var classType = type;
            while (classType != null)
            {
                yield return classType;
                classType = classType.BaseType;
            }

            var interfaceTypes = type.GetInterfaces();
            foreach (var interfaceType in interfaceTypes)
            {
                yield return interfaceType;
            }

            if (this.AdaptService != null)
            {
                classType = type;
                while (classType != null)
                {
                    var adaptTypes = this.AdaptService.GetAdaptTypes(classType);
                    foreach (var adaptType in adaptTypes)
                    {
                        yield return adaptType;
                    }

                    classType = classType.BaseType;
                }

                interfaceTypes = type.GetInterfaces();
                foreach (var interfaceType in interfaceTypes)
                {
                    var adaptTypes = this.AdaptService.GetAdaptTypes(interfaceType);
                    foreach (var adaptType in adaptTypes)
                    {
                        yield return adaptType;
                    }
                }
            }
        }

        #endregion

        // TODO Error on non auto bind any property???
    }
}